import { Store, StoreScope } from '@openops/blocks-framework';
import {
  MAX_REQUEST_BODY_WITH_BUFFER_MB,
  ONE_MB_IN_BYTES,
} from '@openops/server-shared';
import {
  DeleteStoreEntryRequest,
  FlowId,
  FlowRunId,
  PutStoreEntryRequest,
  StoreEntry,
} from '@openops/shared';
import { StatusCodes } from 'http-status-codes';
import { URL } from 'node:url';
import sizeof from 'object-sizeof';
import {
  ExecutionError,
  FetchError,
  StorageError,
  StorageLimitError,
} from '../helper/execution-errors';

export const createStorageService = ({
  engineToken,
  apiUrl,
}: CreateStorageServiceParams): StorageService => {
  return {
    async get(key: string): Promise<StoreEntry | null> {
      const url = buildUrl(apiUrl, key);

      try {
        const response = await fetch(url, {
          headers: {
            Authorization: `Bearer ${engineToken}`,
          },
        });

        if (!response.ok) {
          return await handleResponseError({
            key,
            response,
          });
        }

        return await response.json();
      } catch (e) {
        return handleFetchError({
          url,
          cause: e,
        });
      }
    },

    async put(request: PutStoreEntryRequest): Promise<StoreEntry | null> {
      const url = buildUrl(apiUrl);

      try {
        const sizeOfValue = sizeof(request.value);
        const maxStorageSizeMB = MAX_REQUEST_BODY_WITH_BUFFER_MB;
        const maxStorageSizeBytes = maxStorageSizeMB * ONE_MB_IN_BYTES;

        if (sizeOfValue > maxStorageSizeBytes) {
          throw new StorageLimitError(request.key, maxStorageSizeMB);
        }
        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${engineToken}`,
          },
          body: JSON.stringify(request),
        });

        if (!response.ok) {
          return await handleResponseError({
            key: request.key,
            response,
          });
        }

        return await response.json();
      } catch (e) {
        return handleFetchError({
          url,
          cause: e,
        });
      }
    },

    async delete(request: DeleteStoreEntryRequest): Promise<null> {
      const url = buildUrl(apiUrl, request.key);

      try {
        const response = await fetch(url, {
          method: 'DELETE',
          headers: {
            Authorization: `Bearer ${engineToken}`,
          },
        });

        if (!response.ok) {
          await handleResponseError({
            key: request.key,
            response,
          });
        }

        return null;
      } catch (e) {
        return handleFetchError({
          url,
          cause: e,
        });
      }
    },

    async list(
      prefix: string,
      filterRegex?: string,
    ): Promise<Array<{ key: string; value: unknown }>> {
      const url = new URL(`${apiUrl}v1/store-entries/list`);
      url.searchParams.set('prefix', prefix);

      if (filterRegex) {
        url.searchParams.set('filterRegex', filterRegex);
      }

      try {
        const response = await fetch(url, {
          headers: {
            Authorization: `Bearer ${engineToken}`,
          },
        });

        if (!response.ok) {
          throw new Error(`Failed to list keys: ${response.statusText}`);
        }

        const result = await response.json();
        return result.entries || [];
      } catch (e) {
        return handleFetchError({
          url,
          cause: e,
        });
      }
    },
  };
};

export function createContextStore({
  apiUrl,
  prefix,
  flowId,
  flowRunId,
  engineToken,
}: {
  apiUrl: string;
  prefix: string;
  flowId: FlowId;
  flowRunId: FlowRunId;
  engineToken: string;
}): Store {
  return {
    async put<T>(key: string, value: T, scope = StoreScope.FLOW): Promise<T> {
      const modifiedKey = createKey(prefix, scope, flowId, flowRunId, key);
      await createStorageService({ apiUrl, engineToken }).put({
        key: modifiedKey,
        value,
      });
      return value;
    },
    async delete(key: string, scope = StoreScope.FLOW): Promise<void> {
      const modifiedKey = createKey(prefix, scope, flowId, flowRunId, key);
      await createStorageService({ apiUrl, engineToken }).delete({
        key: modifiedKey,
      });
    },
    async get<T>(key: string, scope = StoreScope.FLOW): Promise<T | null> {
      const modifiedKey = createKey(prefix, scope, flowId, flowRunId, key);
      const storeEntry = await createStorageService({
        apiUrl,
        engineToken,
      }).get(modifiedKey);
      if (storeEntry === null) {
        return null;
      }
      return storeEntry.value as T;
    },
    async list(
      scope: StoreScope,
      keyPrefix: string,
      filterRegex?: string,
    ): Promise<Array<{ key: string; value: unknown }>> {
      const scopePrefix = createKey(
        prefix,
        scope,
        flowId,
        flowRunId,
        keyPrefix,
      );

      const keyValuePairs = await createStorageService({
        apiUrl,
        engineToken,
      }).list(scopePrefix, filterRegex);

      return keyValuePairs.map((entry) => ({
        key: entry.key.replace(scopePrefix, ''),
        value: entry.value,
      }));
    },
  };
}

function createKey(
  prefix: string,
  scope: StoreScope,
  flowId: FlowId,
  flowRunId: FlowRunId,
  key: string,
): string {
  switch (scope) {
    case StoreScope.PROJECT:
      return prefix + key;
    case StoreScope.FLOW:
      return prefix + 'flow_' + flowId + '/' + key;
    case StoreScope.FLOW_RUN:
      return prefix + 'flow_run_' + flowRunId + '/' + key;
  }
}

const buildUrl = (apiUrl: string, key?: string): URL => {
  const url = new URL(`${apiUrl}v1/store-entries`);

  if (key) {
    url.searchParams.set('key', key);
  }

  return url;
};

const handleResponseError = async ({
  key,
  response,
}: HandleResponseErrorParams): Promise<null> => {
  if (response.status === StatusCodes.NOT_FOUND.valueOf()) {
    return null;
  }

  if (response.status === StatusCodes.REQUEST_TOO_LONG) {
    throw new StorageLimitError(key, MAX_REQUEST_BODY_WITH_BUFFER_MB);
  }
  const cause = await response.text();
  throw new StorageError(key, cause);
};

const handleFetchError = ({ url, cause }: HandleFetchErrorParams): never => {
  if (cause instanceof ExecutionError) {
    throw cause;
  }
  throw new FetchError(url.toString(), cause);
};

type CreateStorageServiceParams = {
  engineToken: string;
  apiUrl: string;
};

type StorageService = {
  get(key: string): Promise<StoreEntry | null>;
  put(request: PutStoreEntryRequest): Promise<StoreEntry | null>;
  delete(request: DeleteStoreEntryRequest): Promise<null>;
  list(
    prefix: string,
    filterRegex?: string,
  ): Promise<Array<{ key: string; value: unknown }>>;
};

type HandleResponseErrorParams = {
  key: string;
  response: Response;
};

type HandleFetchErrorParams = {
  url: URL;
  cause: unknown;
};
